/**
 * 
 */
package cyclopean.world;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import javagame.core.io.video.shader.Shader;
import javagame.core.main.Game;

import javax.imageio.ImageIO;
import javax.media.opengl.GL;

import org.apache.log4j.Logger;

import cyclopean.world.Zone.ZoneListener;

// TODO Javadoc
/**
 * 
 *
 * @author Jaco van der Westhuizen
 */
class ZoneRenderer implements ZoneListener
{
	private static final Logger logger = Logger.getLogger(ZoneRenderer.class);

	static final int[] glVBO = new int[3];
	static final int ARRAY_POS  = 0;	// Vertex positions
	static final int ARRAY_NORM = 1;	// Vertex normals
	static final int ARRAY_TCO  = 2;	// Texture coordinate offset
	static final int ARRAY_VBO_SIZE = (Zone.sizeX + Zone.sizeY + Zone.sizeZ) * 2 * 4;	//	(sizeX + sizeY + sizeZ) quads, 2 sides per quad, 4 verts per side

	static final int[] glTex = new int[1];
	static final int TEX_DETAIL = 16;
	
	static boolean initStatic = false;
	
	private static void init()
	{
		GL gl = Game.get().grabGL();

		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glEnable(GL.GL_TEXTURE_3D);
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glEnable(GL.GL_BLEND);
		gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glDepthFunc(GL.GL_LEQUAL);

		// Generate texture atlas for block textures
		final int sizeX = TEX_DETAIL * BlockType.X_COUNT * 2;
		final int sizeY = TEX_DETAIL * BlockType.Y_COUNT * 2;
		
		gl.glGenTextures(1, glTex, 0);

		gl.glBindTexture(GL.GL_TEXTURE_2D, glTex[0]);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, 4, sizeX, sizeY, 0, GL.GL_RGBA, GL.GL_UNSIGNED_INT_8_8_8_8, null);//IntBuffer.wrap(new int[sizeX * sizeY]));
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);

		// Generate VBO data buffers
		createVBO(gl);

		Game.get().releaseGL();
		
		initStatic = true;
	}

	/**
     * 
     */
    private static void createVBO(GL gl)
    {
		// Create vertex data arrays
		gl.glGenBuffers(glVBO.length, glVBO, 0);

		final FloatBuffer pos  = FloatBuffer.allocate(ARRAY_VBO_SIZE * 3);
		final FloatBuffer norm = FloatBuffer.allocate(ARRAY_VBO_SIZE * 3);
		final FloatBuffer tco  = FloatBuffer.allocate(ARRAY_VBO_SIZE * 2);
		final float[] normPut = new float[3];
		final float[]  tcoPut = new float[2];

		// X+ (Looking East)
		normPut[0] = -1;
		normPut[1] =  0;
		normPut[2] =  0;
		tcoPut[0] = 0.5f / BlockType.X_COUNT;
		tcoPut[1] = 0.5f / BlockType.Y_COUNT;

		for (int x = 0; x < Zone.sizeX; x++)
		{
			pos.put(x);
			pos.put(0);
			pos.put(0);
			pos.put(x);
			pos.put(0);
			pos.put(Zone.sizeZ);
			pos.put(x);
			pos.put(Zone.sizeY);
			pos.put(Zone.sizeZ);
			pos.put(x);
			pos.put(Zone.sizeY);
			pos.put(0);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		// X- (Looking West)
		normPut[0] = 1;
		normPut[1] = 0;
		normPut[2] = 0;
		tcoPut[0] = 0.5f / BlockType.X_COUNT;
		tcoPut[1] = 0.5f / BlockType.Y_COUNT;

		for (int x = Zone.sizeX; x > 0; x--)
		{
			pos.put(x);
			pos.put(0);
			pos.put(0);
			pos.put(x);
			pos.put(Zone.sizeY);
			pos.put(0);
			pos.put(x);
			pos.put(Zone.sizeY);
			pos.put(Zone.sizeZ);
			pos.put(x);
			pos.put(0);
			pos.put(Zone.sizeZ);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		// Y+ (Looking North)
		normPut[0] =  0;
		normPut[1] = -1;
		normPut[2] =  0;
		tcoPut[0] = 0.5f / BlockType.X_COUNT;
		tcoPut[1] = 0;

		for (int y = 0; y < Zone.sizeY; y++)
		{
			pos.put(0);
			pos.put(y);
			pos.put(0);
			pos.put(Zone.sizeX);
			pos.put(y);
			pos.put(0);
			pos.put(Zone.sizeX);
			pos.put(y);
			pos.put(Zone.sizeZ);
			pos.put(0);
			pos.put(y);
			pos.put(Zone.sizeZ);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		// Y- (Looking South)
		normPut[0] = 0;
		normPut[1] = 1;
		normPut[2] = 0;
		tcoPut[0] = 0.5f / BlockType.X_COUNT;
		tcoPut[1] = 0;

		for (int y = Zone.sizeY; y > 0; y--)
		{
			pos.put(0);
			pos.put(y);
			pos.put(0);
			pos.put(0);
			pos.put(y);
			pos.put(Zone.sizeZ);
			pos.put(Zone.sizeX);
			pos.put(y);
			pos.put(Zone.sizeZ);
			pos.put(Zone.sizeX);
			pos.put(y);
			pos.put(0);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		// Z+ (Looking Up)
		normPut[0] =  0;
		normPut[1] =  0;
		normPut[2] = -1;
		tcoPut[0] = 0;
		tcoPut[1] = 0.5f / BlockType.Y_COUNT;

		for (int z = 0; z < Zone.sizeZ; z++)
		{
			pos.put(0);
			pos.put(0);
			pos.put(z);
			pos.put(0);
			pos.put(Zone.sizeY);
			pos.put(z);
			pos.put(Zone.sizeX);
			pos.put(Zone.sizeY);
			pos.put(z);
			pos.put(Zone.sizeX);
			pos.put(0);
			pos.put(z);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		// Z- (Looking Down)
		normPut[0] = 0;
		normPut[1] = 0;
		normPut[2] = 1;
		tcoPut[0] = 0;
		tcoPut[1] = 0;

		for (int z = Zone.sizeZ; z > 0; z--)
		{
			pos.put(0);
			pos.put(0);
			pos.put(z);
			pos.put(Zone.sizeX);
			pos.put(0);
			pos.put(z);
			pos.put(Zone.sizeX);
			pos.put(Zone.sizeY);
			pos.put(z);
			pos.put(0);
			pos.put(Zone.sizeY);
			pos.put(z);
			
			for (int i = 0; i < 4; i++)
			{
				norm.put(normPut);
				tco.put(tcoPut);
			}
		}

		pos.rewind();
		norm.rewind();
		tco.rewind();

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_POS]);
		gl.glBufferData(GL.GL_ARRAY_BUFFER, pos.capacity() * Float.SIZE / 8, pos, GL.GL_STATIC_DRAW);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_NORM]);
		gl.glBufferData(GL.GL_ARRAY_BUFFER, norm.capacity() * Float.SIZE / 8, norm, GL.GL_STATIC_DRAW);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_TCO]);
		gl.glBufferData(GL.GL_ARRAY_BUFFER, tco.capacity() * Float.SIZE / 8, tco, GL.GL_STATIC_DRAW);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
    }

	static void addBlockType(byte id, String fileName)
	{
		if (!initStatic)
			init();
		
		final File file = new File(fileName);
		BufferedImage img = null;

		try
		{
			img = ImageIO.read(file);
		}
		catch (IOException x)
		{
			logger.error("Failed to load image for block number " + id + ", '" + file.getName() + "'");
			return;
		}

		final int[] rgba = new int[TEX_DETAIL * TEX_DETAIL * 4];
		int x = (id % BlockType.X_COUNT) * TEX_DETAIL * 2;
		int y = (id / BlockType.X_COUNT) * TEX_DETAIL * 2;

		GL gl = Game.get().grabGL();

		img.getRGB(0, 0, TEX_DETAIL * 2, TEX_DETAIL * 2, rgba, 0, 32);

		gl.glBindTexture(GL.GL_TEXTURE_2D, glTex[0]);
		gl.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, x, y, TEX_DETAIL * 2, TEX_DETAIL * 2, GL.GL_RGBA, GL.GL_UNSIGNED_INT_8_8_8_8, IntBuffer.wrap(rgba));

		Game.get().releaseGL();
	}

	static void unload(GL gl)
	{
		// Unload VBO
		gl.glDeleteBuffers(glVBO.length, glVBO, 0);
		
		// Unload textures
		gl.glDeleteTextures(glTex.length, glTex, 0);
	}

//----------------------------------------------------------------------------------------------------------------------	

	final Zone zone;
	boolean zoneChanged = true;
	
	private final int[] glDataTex = new int[2];
	private static final int DATA_BLOCKTYPE_AND_FLAGS = 0;
	private static final int DATA_LIGHT_COLOR = 1;

	private Shader shader = null;
	
	ZoneRenderer(GL gl, Zone zone)
	{
		this.zone = zone;
		
		zone.listeners.add(this);
		
		gl.glGenTextures(glDataTex.length, glDataTex, 0);
		
		gl.glBindTexture(GL.GL_TEXTURE_3D, glDataTex[DATA_BLOCKTYPE_AND_FLAGS]);
		gl.glTexImage3D(GL.GL_TEXTURE_3D, 0, 4, Zone.sizeX, Zone.sizeY, Zone.sizeZ, 0, GL.GL_RGBA, GL.GL_UNSIGNED_INT_8_8_8_8, null);
		gl.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_R, GL.GL_CLAMP);
	}
	
	void render(GL gl)
	{
		if (!initStatic)
			return;

		if (shader == null)
		{
			// Load shader
			try
	        {
		        shader = new BlocksShader(gl, glDataTex[DATA_BLOCKTYPE_AND_FLAGS], glDataTex[DATA_LIGHT_COLOR], glTex[0]);
	        }
	        catch (IOException e)
	        {
		        logger.fatal("Failed to load blocks shader programs.  Making appropriate mess in response.  :^)", e);
		        shader = null;
	        }
		}

		// Update the data texture(s)
		if (zoneChanged)
		{
			gl.glBindTexture(GL.GL_TEXTURE_3D, glDataTex[DATA_BLOCKTYPE_AND_FLAGS]);
			zone.dataBuffer.rewind();
			gl.glTexSubImage3D(GL.GL_TEXTURE_3D, 0, 0, 0, 0, Zone.sizeX, Zone.sizeY, Zone.sizeZ, GL.GL_ALPHA, GL.GL_UNSIGNED_INT_8_8_8_8, zone.dataBuffer);
			
			zoneChanged = false;
		}

		// Activate the shader program
		shader.activate(gl);
		shader.prepare(gl);

		// Render the VBO
		gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
		gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
		gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_POS]);
		gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_NORM]);
		gl.glNormalPointer(GL.GL_FLOAT, 0, 0);
		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY_TCO]);
		gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0);

		gl.glDrawArrays(GL.GL_QUADS, 0, ARRAY_VBO_SIZE);
		
		gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
		gl.glDisableClientState(GL.GL_NORMAL_ARRAY);
		gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
		
		Shader.deactivate(gl);
	}

	/* (non-Javadoc)
     * @see cyclopean.world.Zone.ZoneListener#onChange(cyclopean.world.Zone)
     */
    @Override
    public void onChange(Zone zone)
    {
    	if (zone == this.zone)
    		zoneChanged = true;
    }
}
